package com.healthcarex.hip.saas.message.hl7.v3.processor;

import java.io.IOException;
import java.util.*;
import java.util.Map.Entry;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;

import com.healthcarex.hip.saas.message.core.AbstractMessage;
import com.healthcarex.hip.saas.message.core.MessageModel;
import com.healthcarex.hip.saas.message.exception.MessageException;
import com.healthcarex.hip.saas.message.factory.ValueHandlerFactory;
import com.healthcarex.hip.saas.message.handler.IValueHandler;
import com.healthcarex.hip.saas.message.hl7.v3.core.HL7V3Constant;
import com.healthcarex.hip.saas.message.hl7.v3.core.HL7V3Message;
import com.healthcarex.hip.saas.message.hl7.v3.namespace.AbstractNamespaceContext;
import com.healthcarex.hip.saas.message.hl7.v3.namespace.MapNamespaceContext;
import com.healthcarex.hip.saas.message.hl7.v3.util.HL7V3Util;
import com.healthcarex.hip.saas.message.hl7.v3.util.XmlXPathUtil;
import com.healthcarex.hip.saas.message.schema.MessageSchemaDefinition;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;


public class HL7V3MessageProcessor extends XMLMessageProcessor {
    private static final Logger logger = LoggerFactory
            .getLogger(HL7V3MessageProcessor.class);

    private XmlXPathUtil xpathUtil = new XmlXPathUtil();
    private AbstractNamespaceContext nsc = new MapNamespaceContext();

    /**
     *
     */
    protected AbstractNamespaceContext getNamesapceContext() {
        return nsc;
    }

    /**
     *
     */
    @Override
    public String getMessageType() {
        return "HL7_V3_MESSAGE";
    }

    /**
     * @param message
     * @param msd     msd
     */
    @Override
    public AbstractMessage generate(MessageModel message,
                                    MessageSchemaDefinition msd) {
        if (message != null && msd != null) {
            try {
                Map<String, Object> content = message.getContent();
                Map<String, Object> schema = msd.getSchema();
                String xmlTemplate = msd.getTemplate();

                if (content == null || schema == null
                        || StringUtils.isEmpty(xmlTemplate)) {
                    return null;
                }

                Document doc = stringToDocument(xmlTemplate);
                AbstractNamespaceContext nsc = getNamesapceContext();

                fillNode(doc, schema, nsc, content);
                HL7V3Message outMessage = new HL7V3Message(
                        documentToString(doc), msd);

                return outMessage;
            } catch (ParserConfigurationException e) {
                logger.error(e.toString());
            } catch (SAXException e) {
                logger.error(e.toString());
            } catch (IOException e) {
                logger.error(e.toString());
            } catch (TransformerException e) {
                logger.error(e.toString());
            } catch (MessageException e) {
                logger.error(e.toString());
            }
        }

        return null;
    }

    /**
     * dom4j
     *
     * @param message message
     */
    @Override
    public MessageModel parse(AbstractMessage message) throws Exception {
        if (message != null) {
            String orgMessage = (String) message.getMessage();
            MessageSchemaDefinition msd = message.getMessageSchemaDef();

            if (StringUtils.isEmpty(orgMessage) || msd == null) {
                return null;
            }

            Map<String, Object> schema = msd.getSchema();

            if (schema == null) {
                return null;
            }

            try {
                Document doc = stringToDocument(orgMessage);
                AbstractNamespaceContext nsc = getNamesapceContext();

                MessageModel gm = new MessageModel();
                gm.setContent(getNodeContent(doc, schema, nsc));

                return gm;
            } catch (ParserConfigurationException e) {
                logger.error(e.toString());
                throw new ParserConfigurationException(e.toString());
            } catch (SAXException e) {
                logger.error(e.toString());
                throw new SAXException(e.toString());
            } catch (IOException e) {
                logger.error(e.toString());
                throw new IOException(e.toString());
            }
        }

        return null;
    }

    /**
     * @param node
     * @param schema
     * @param nsc
     * @return
     */
    @SuppressWarnings("unchecked")
    private Map<String, Object> getNodeContent(Node node, Object schema,
                                               AbstractNamespaceContext nsc) {
        if (node != null && schema instanceof Map && nsc != null) {
            Map<String, Object> mapResult = new HashMap<String, Object>();
            String key;
            Object value;

            for (Entry<String, Object> entity : ((Map<String, Object>) schema)
                    .entrySet()) {
                key = entity.getKey();
                Map<String, Object> rule = (Map<String, Object>) entity
                        .getValue();
                value = processReadingSchema(rule, node, nsc);
                mapResult.put(key, value);
            }

            return mapResult;
        }

        return null;
    }

    /**
     * @param node
     * @param rule
     * @param nsc
     * @return
     */
    @SuppressWarnings("unchecked")
    private Object processReadingSchema(Map<String, Object> rule, Node node,
                                        AbstractNamespaceContext nsc) {
        if (node != null && rule instanceof Map && nsc != null) {
            String xpathStr = (String) rule.get(HL7V3Constant.SCHEMA_KEY_PATH);
            NodeList nodeList = xpathUtil.getListValue(xpathStr, node, nsc);

            // rule.get(HL7V3Constant.SCHEMA_KEY_DESC);
            // rule.get(HL7V3Constant.SCHEMA_KEY_DISPLAY);
            Map<String, Map<String, Object>> child = (Map<String, Map<String, Object>>) rule
                    .get(HL7V3Constant.SCHEMA_KEY_CHILD);
            boolean hasChild = hasChild(rule);
            String multiplicity = (String) rule
                    .get(HL7V3Constant.SCHEMA_KEY_MULTIPLICITY);

            if (HL7V3Util.isSingleNode(multiplicity)) {
                if (nodeList.getLength() > 0) {
                    Node tmpNode = (Node) nodeList.item(0);
                    if (hasChild) {
                        return getNodeContent(tmpNode, child, nsc);
                    }

                    return getNodeValue(tmpNode, rule);
                } else {
                    return null;
                }
            } else {
                List<Object> listResult = new ArrayList<>();
                for (int i = 0; i < nodeList.getLength(); i++) {
                    Node tmpNode = nodeList.item(i);
                    if (hasChild) {
                        listResult.add(getNodeContent(tmpNode, child, nsc));
                    } else {
                        listResult.add(getNodeValue(tmpNode, rule));
                    }
                }

                return listResult;
            }
        }

        return null;
    }

    /**
     * @param message
     * @param msd     msd
     */
    @Override
    protected boolean verify(Object message, MessageSchemaDefinition msd) {
        return true;
    }

    /**
     * @param rule
     * @return
     */
    @SuppressWarnings("unchecked")
    private boolean hasChild(Map<String, Object> rule) {
        // rule.get(HL7V3Constant.SCHEMA_KEY_TYPE);
        Map<String, Map<String, Object>> child = (Map<String, Map<String, Object>>) rule
                .get(HL7V3Constant.SCHEMA_KEY_CHILD);

        if (child != null) {
            return true;
        }

        return false;
    }

    /**
     * @param node
     * @param rule
     * @return
     */
    private Object getNodeValue(Node node, Map<String, Object> rule) {
        String format = (String) rule.get(HL7V3Constant.SCHEMA_KEY_FORMAT);
        String type = (String) rule.get(HL7V3Constant.SCHEMA_KEY_TYPE);
        IValueHandler valueHandler = ValueHandlerFactory.getValueHandler(type);
        Object value = node.getTextContent();

        if (valueHandler != null) {
            value = valueHandler.getReadingValue(value, type, format);
        }

        return value != null ? value : (String) rule
                .get(HL7V3Constant.SCHEMA_KEY_DEFAULT_VALUE);
    }

    /**
     * @param node
     * @param value
     * @param rule
     * @return
     */
    private boolean setNodeValue(Node node, Object value,
                                 Map<String, Object> rule) {
        String format = (String) rule.get(HL7V3Constant.SCHEMA_KEY_FORMAT);
        String type = (String) rule.get(HL7V3Constant.SCHEMA_KEY_TYPE);
        IValueHandler valueHandler = ValueHandlerFactory.getValueHandler(type);
        String expressionStr = (String) rule.get(HL7V3Constant.SCHEMA_KEY_EXPRESSION);
        if (StringUtils.isNotEmpty(expressionStr)) {
            ExpressionParser parser = new SpelExpressionParser();
            value = parser.parseExpression(expressionStr);
        }
        String retValue = value != null ? value.toString() : "";
        if (valueHandler != null) {
            retValue = valueHandler.getWritingValue(value, format);
        }
        node.setTextContent(retValue);

        return true;
    }

    /**
     * @param node
     * @param schema
     * @param nsc
     * @param content
     * @return
     * @throws MessageException
     */
    private boolean fillNode(Node node, Object schema,
                             AbstractNamespaceContext nsc, Object content)
            throws MessageException {
        if ((node != null) && ((schema instanceof Map)) && (nsc != null) && ((content instanceof Map))) {
            String key;
            Object value;
            for (Map.Entry<String, Object> entity : ((Map<String, Object>) schema).entrySet()) {
                key = entity.getKey();
                value = ((Map) content).get(key);
                Map<String, Object> rule = (Map) entity.getValue();
                if ((value == null) || ("".equals(value))) {
                    String controlNode = (String) rule.get(HL7V3Constant.SCHEMA_KEY_CONTROL_NODE);
                    NodeList nodeList = this.xpathUtil.getListValue(controlNode, node, nsc);
                    if (nodeList != null) {
                        Node parentNode = nodeList.item(0).getParentNode();
                        for (int i = 0; i < nodeList.getLength(); i++) {
                            Node oldChild = nodeList.item(i);
                            try {
                                parentNode.removeChild(oldChild);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }
                } else {
                    processWritingSchema(rule, node, nsc, value);
                }
            }
            return true;
        }
        return false;
    }

    /**
     * @param node
     * @param rule
     * @param nsc
     * @param content
     * @return
     * @throws MessageException
     */
    private void processWritingSchema(Map<String, Object> rule, Node node,
                                      AbstractNamespaceContext nsc, Object content)
            throws MessageException {
        if (node != null && rule instanceof Map && nsc != null
                && content != null) {
            String xpathStr = (String) rule.get(HL7V3Constant.SCHEMA_KEY_PATH);
            NodeList nodeList = xpathUtil.getListValue(xpathStr, node, nsc);

            if ((nodeList == null) || (nodeList.getLength() <= 0)) {
                return;
            }
            Map<String, Map<String, Object>> child = (Map<String, Map<String, Object>>) rule
                    .get(HL7V3Constant.SCHEMA_KEY_CHILD);
            boolean hasChild = hasChild(rule);
            String multiplicity = (String) rule
                    .get(HL7V3Constant.SCHEMA_KEY_MULTIPLICITY);

            if (HL7V3Util.isSingleNode(multiplicity)) {
                // single node
                Object value = null;
                if (content instanceof Collection) {
                    Object[] contentList = ((Collection<?>) content).toArray();
                    if (contentList.length > 0) {
                        value = contentList[0];
                    }
                } else {
                    value = content;
                }

                if (nodeList.getLength() == 1) {
                    Node tmpNode = (Node) nodeList.item(0);
                    if (value != null) {
                        if (hasChild && content instanceof Collection) {
                            fillNode(tmpNode, child, nsc, value);
                        }

                        setNodeValue(tmpNode, value, rule);
                    } else {
                        // TODO
                        // delete node
                        // tmpNode.getParentNode().removeChild(tmpNode);
                    }
                } else {
                    throw new MessageException("path:[" + xpathStr
                            + "]. Can not get content from message:[" + content
                            + "]");
                }
            } else {
                // multiple node
                if (content instanceof Collection) {
                    Object[] contentList = ((Collection<?>) content).toArray();
                    int nodeCount = nodeList.getLength();
                    int contentCount = contentList.length;
                    boolean cloneFlag = isNeedCopy(nodeCount, contentCount);

                    if ((nodeCount == contentCount && nodeCount > 0)
                            || cloneFlag) {
                        Node cloneNode = null;
                        Node templateNode = nodeList.item(0);
                        Node parentNode = templateNode.getParentNode();

                        for (int i = 0; i < contentCount; i++) {
                            if (cloneFlag && i > 0) {
                                cloneNode = templateNode.cloneNode(true);
                                parentNode.appendChild(cloneNode);
                            } else {
                                cloneNode = nodeList.item(i);
                            }

                            if (hasChild) {
                                fillNode(cloneNode, child, nsc, contentList[i]);
                            } else {
                                setNodeValue(cloneNode, contentList[i], rule);
                            }
                        }
                    } else if (contentCount == 0) {
                        // TODO
                        // delete node
                        // Node parentNode = nodeList.item(0).getParentNode();
                        // for (int i = 0; i < nodeList.getLength(); i++) {
                        // Node oldChild = nodeList.item(i);
                        // parentNode.removeChild(oldChild);
                        // }
                    } else {
                        throw new MessageException("path:[" + xpathStr
                                + "]. Can not get content from message:["
                                + content + "]");
                    }
                } else {
                    throw new MessageException("path:[" + xpathStr
                            + "]. Can not get content from message:[" + content
                            + "]");
                }
            }
        }
    }

    /**
     * @param nodeCount
     * @param contentCount
     * @return
     */
    private boolean isNeedCopy(int nodeCount, int contentCount) {
        return (nodeCount == 1 && contentCount >= nodeCount);
    }
}
